﻿using System;
using System.Collections.Generic;
using System.Threading;
using System.ComponentModel;

namespace Scheduler
{
    /// <summary>
    /// The scheduler executes items of work at the (approximately) appropriately scheduled time
    /// </summary>
    [Serializable(), Browsable(true), TypeConverter(typeof(ExpandableObjectConverter))]
    public class SchedulingEngine : ISchedulingEngine
    {
        /// <summary>
        /// Defines the total number of threads to be allocated to this scheduler/engine
        /// In general, more threads are needed as the execution of each work item increases
        /// to avoid long work items blocking other scheduled work
        /// </summary>
        private readonly int TotalThreads;
        private readonly int ModerateDelayQueueCutoffMS;
        private readonly int LargeDelayQueueCutoffMS;
        private readonly int MaxExceptionListSize;
        private readonly int CheckQueueIntervalMS;
        private readonly int QueueIdleSleepTimeMS;

        private List<Exception> _exceptions;
        private DateTime _currentDateTime;
        internal ThreadSafeQueue<IExecutableWorkItem>[] _allWorkItemQueues;
        private WorkItemQueueHandler[] _workItemQueueHandlers;
        private int counter;
        private long _executed;

        public event EventHandler WorkItemQueuesEmpty;
        private long _workItemCount;
        public ISupportsCount[] WorkItemQueueCounts { get { return _allWorkItemQueues.ToInterfaceArray<ISupportsCount, ThreadSafeQueue<IExecutableWorkItem>>(); } }
        public List<Exception> Exceptions { get { return _exceptions; } set { _exceptions = value; } }
        public long WorkItemsExecuted { get { return _executed; } set { _executed = value; } }
        public bool WantExit { get; set; }
        public DateTime CurrentDateTime { get { return _currentDateTime; } set { _currentDateTime = value; } }

        private void Initialize()
        {
            _allWorkItemQueues = new ThreadSafeQueue<IExecutableWorkItem>[TotalThreads];
            _workItemQueueHandlers = new WorkItemQueueHandler[TotalThreads];
            _exceptions = new List<Exception>();
            _currentDateTime = DateTime.Now;
        }

        public SchedulingEngine()
        {
            //TotalThreads = Properties.Settings.Default.WorkerThreadCount;
            //ModerateDelayQueueCutoffMS = Properties.Settings.Default.ModerateDelayQueueCutoffMS;
            //LargeDelayQueueCutoffMS = Properties.Settings.Default.LargeDelayQueueCutoffMS;
            //MaxExceptionListSize = Properties.Settings.Default.MaxExceptionListSize;
            //CheckQueueIntervalMS = Properties.Settings.Default.CheckQueueIntervalMS;
            //QueueIdleSleepTimeMS = Properties.Settings.Default.QueueIdleSleepTimeMS;

            TotalThreads = 34;
            ModerateDelayQueueCutoffMS = 50;
            LargeDelayQueueCutoffMS = 500;
            MaxExceptionListSize = 3000;
            CheckQueueIntervalMS = 32;
            QueueIdleSleepTimeMS = 16;

            Initialize();
        }

        public SchedulingEngine(int workerThreadCount)
        {
            TotalThreads = workerThreadCount;
            //ModerateDelayQueueCutoffMS = Properties.Settings.Default.ModerateDelayQueueCutoffMS;
            //LargeDelayQueueCutoffMS = Properties.Settings.Default.LargeDelayQueueCutoffMS;
            //MaxExceptionListSize = Properties.Settings.Default.MaxExceptionListSize;
            //CheckQueueIntervalMS = Properties.Settings.Default.CheckQueueIntervalMS;
            //QueueIdleSleepTimeMS = Properties.Settings.Default.QueueIdleSleepTimeMS;

            ModerateDelayQueueCutoffMS = 50;
            LargeDelayQueueCutoffMS = 500;
            MaxExceptionListSize = 3000;
            CheckQueueIntervalMS = 32;
            QueueIdleSleepTimeMS = 16;
            Initialize();

        }

        /// <summary>
        /// Starts up the scheduling engine
        /// </summary>
        public void StartSchedulingEngine()
        {
            for (int n = 0; n < _allWorkItemQueues.Length; n++)
            {
                _allWorkItemQueues[n] = new ThreadSafeQueue<IExecutableWorkItem>();
                _workItemQueueHandlers[n] = new WorkItemQueueHandler(this, n);
                _workItemQueueHandlers[n].StartEventHandler();
            }
        }

        /// <summary>
        /// Schedules a work item to execute at a specific time
        /// </summary>
        /// <param m_strName="eventToExecute"></param>
        public void ScheduleEventToExecute(IExecutableWorkItem workItemToExecute)
        {
            int queueNum = FindAppropriateQueue(workItemToExecute);
            _allWorkItemQueues[queueNum].Enqueue(workItemToExecute);
            Interlocked.Increment(ref _workItemCount);
        }

        /// <summary>
        /// Determines the appropriate queue in which to place the work item, based on its
        /// scheduled execution time
        /// </summary>
        /// <param m_strName="eventToExecute"></param>
        /// <returns></returns>
        private int FindAppropriateQueue(IExecutableWorkItem workItemToExecute)
        {
            TimeSpan ts = workItemToExecute.ExecutionTime.Subtract(CurrentDateTime);
            if (ts.TotalMilliseconds > LargeDelayQueueCutoffMS)
                return TotalThreads - 1;
            else if (ts.TotalMilliseconds > ModerateDelayQueueCutoffMS)
                return TotalThreads - 2;
            else
                return Interlocked.Increment(ref counter) % (TotalThreads - 2);
        }

        private int UpdateWorkItemSchedule(int queueNumber)
        {
            int workItemsMoved = 0;
            IExecutableWorkItem item;

            // Store items that need to go back into this 'slow-track' queue
            List<IExecutableWorkItem> itemsBackIntoOriginalQueue = new List<IExecutableWorkItem>();
            List<IExecutableWorkItem> workItems = new List<IExecutableWorkItem>(16);

            while (_allWorkItemQueues[queueNumber].DequeueMultiple(workItems, 10) > 0 && !WantExit)
            {
                for (int n = 0; n < workItems.Count; n++)
                {
                    item = workItems[n];

                    // Determine the appropriate work item queue for this item, based on its scheduled execution time
                    int appropriateQueue = FindAppropriateQueue(item);

                    // Check if this item needs to be moved into a different queue
                    if (queueNumber != appropriateQueue)
                    {
                        _allWorkItemQueues[appropriateQueue].Enqueue(item);
                        workItemsMoved++;
                    }
                    else
                    {
                        // We will need to put the item back into the original queue
                        itemsBackIntoOriginalQueue.Add(item);
                    }
                }

                workItems.Clear();
            }

            // Return the work items that did not need to be moved to the slow-track queue
            _allWorkItemQueues[queueNumber].EnqueueMultiple(itemsBackIntoOriginalQueue);

            return workItemsMoved;
        }


        internal void HandleWorkItemQueue(int n)
        {
            DateTime lastUpdateQ = DateTime.Now;

            while (!WantExit)
            {
                try
                {
                    // Update the current time
                    DateTime currentTime = UpdateDateTime();

                    // If this is a immediate worker thread (work items scheduled to be executed soon)
                    if (n < TotalThreads - 2)
                    {
                        if (lastUpdateQ.AddMilliseconds(CheckQueueIntervalMS) <= currentTime)
                        {
                            lastUpdateQ = currentTime;
                            int executedThisPass = ExecuteEventsInQueue(n);

                            // If there was nothing to execute, sleep
                            if (executedThisPass == 0)
                                Thread.Sleep(CheckQueueIntervalMS);
                        }
                        else
                        {
                            Thread.Sleep(QueueIdleSleepTimeMS);
                        }
                    }
                    else if (n == TotalThreads - 2)
                    {
                        // This holds the "medium-term" work items - scheduled to be executed somewhat soon, but not immediately
                        if (lastUpdateQ.AddMilliseconds(ModerateDelayQueueCutoffMS - 50) <= currentTime)
                        {
                            lastUpdateQ = currentTime;
                            int updated = UpdateWorkItemSchedule(n);

                            if (updated == 0)
                                Thread.Sleep(100);
                        }
                        else
                        {
                            Thread.Sleep(50);
                        }
                    }
                    else if (n == TotalThreads - 1)
                    {
                        // This holds the "long-term" work items - scheduled to be executed later
                        if (lastUpdateQ.AddMilliseconds(LargeDelayQueueCutoffMS - 100) <= currentTime)
                        {
                            lastUpdateQ = currentTime;
                            int updated = UpdateWorkItemSchedule(n);

                            if (updated == 0)
                                Thread.Sleep(200);
                        }
                        else
                        {
                            Thread.Sleep(100);
                        }
                    }
                }
                catch (Exception ex)
                {
                    if (_exceptions.Count > MaxExceptionListSize)
                    {
                        try { _exceptions.RemoveRange(4, MaxExceptionListSize - 5); }
                        catch { }
                    }

                    _exceptions.Add(ex);
                }
            }
        }

        private int ExecuteEventsInQueue(int queueNumber)
        {
            IExecutableWorkItem item;
            DateTime currentTime = CurrentDateTime;

            // Stores executable work items whose execution time has not yet come - and need to be placed back in the queue
            List<IExecutableWorkItem> reschedule = new List<IExecutableWorkItem>();

            // Stores Dequeue'd work items - Dequeues multiple items to reduce repeated calls to Dequeue (and to reduce locking)
            List<IExecutableWorkItem> itemsToExecute = new List<IExecutableWorkItem>(16);

            // Keep track of work item executions this pass
            int executedThisPass = 0;

            // Dequeue multiple work items from the queue
            while (_allWorkItemQueues[queueNumber].DequeueMultiple(itemsToExecute, 10) > 0)
            {
                // Check each dequeue'd work item
                for (int n = 0; n < itemsToExecute.Count && !WantExit; n++)
                {
                    item = itemsToExecute[n];

                    if (item.ExecutionTime > currentTime)
                    {
                        // Execution time for the work item is still in the future
                        reschedule.Add(item);
                    }
                    else
                    {
                        // It is time to execute this work item.  Do it.
                        executedThisPass++;
                        item.Execute();
                    }
                }

                itemsToExecute.Clear();
            }

            // Re-queue all work items that were dequeue'd but not executed (not yet their time)
            _allWorkItemQueues[queueNumber].EnqueueMultiple(reschedule);

            // Add to the executed work item total
            if (executedThisPass > 0)
            {
                long execThisPass = Convert.ToInt64(executedThisPass);
                Interlocked.Add(ref _executed, execThisPass);
                Interlocked.Add(ref _workItemCount, -execThisPass);

                if (_workItemCount == 0)
                {
                    if (WorkItemQueuesEmpty != null)
                    {
                        WorkItemQueuesEmpty(this, new EventArgs());
                    }
                }
            }

            return executedThisPass;
        }

        private DateTime UpdateDateTime()
        {
            _currentDateTime = DateTime.Now;
            return _currentDateTime;
        }
    }
}
